from uuid import uuid4

from sqlalchemy import Column, String
from sqlalchemy.ext.declarative import declared_attr

from apps.utils import pluralize, title_case, model_dict_clear
from apps.utils import to_camel
from .db import db
from .types import DE_FIELD_TYPE_DICT, FIELD_TYPE_DICT


def get_id():
    return str(uuid4())


class PrimaryKeyMixin(object):
    """
    Adds automatically uuid1 type attr:`id` primary key column to a Model
    """
    id = Column(String(36), primary_key=True)

    def __init__(self):
        super(PrimaryKeyMixin, self).__init__()
        self.id = get_id()


class BaseModel(db.Model, PrimaryKeyMixin):
    """Base table class. It includes convenience methods for creating,
    querying, saving, updating and deleting models.
    """
    __abstract__ = True
    __table_args__ = {'extend_existing': True}

    __repr_props__ = ()

    __non_init_props__ = ['id', 'created_time', 'created_user', 'last_edited_time', 'last_edited_user',
                          'last_edited_ip']

    """Set to customize automatic string representation.

    For example::

        class User(database.Model):
            __repr_props__ = ('id', 'email')

            email = Column(String)

        user = User(id=1, email='foo@bar.com')
        print(user)  # prints <User id=1 email="foo@bar.com">
    """

    def __init__(self):
        super(BaseModel, self).__init__()
        PrimaryKeyMixin.__init__(self)

    @classmethod
    def __column_name_list__(cls):

        # cached_column_name_list = cache.get(cls.__tablename__ + '_column_name_list')
        # if cached_column_name_list:
        #     return pickle.loads(cached_column_name_list)
        column_collection_str_list = cls.__table__.__dict__['columns'].__str__().split("'")
        column_name_list = []
        for a in range(0, len(column_collection_str_list)):
            if a % 2 != 0:
                column_name_list.append(column_collection_str_list[a].split('.')[1])
        return column_name_list

    @classmethod
    def __column_list__(cls):

        # cached_column_list = cache.get(cls.__tablename__+'_column_list')
        # if cached_column_list:
        #     return pickle.loads(cached_column_list)
        column_name_list = cls.__column_name_list__()

        column_list = [getattr(cls.__table__.__dict__['columns'], column_name) for column_name in column_name_list]
        return column_list

    @classmethod
    def __serialize_fields__(cls):
        # cached_class_fields = cache.get(cls.__tablename__ + '_serialize_fields')
        # if cached_class_fields:
        #     return pickle.loads(cached_class_fields)
        class_fields = {}
        column_list = cls.__column_list__()
        for column in column_list:
            if column.name == 'password':
                continue
            class_fields.update({
                to_camel(column.name): FIELD_TYPE_DICT.get(type(column.type))(attribute=column.name)
            })
        return class_fields

    @classmethod
    def __deserialize_fields__(cls):
        class_fields = {}
        column_list = cls.__column_list__()
        for column in column_list:
            if column.name == 'password':
                continue
            class_fields.update({
                column.name: DE_FIELD_TYPE_DICT.get(type(column.type))(attribute=to_camel(column.name))
            })
        return class_fields

    @declared_attr
    def __plural__(self):
        return pluralize(self.__name__)

    @declared_attr
    def __label__(self):
        return title_case(self.__name__)

    @declared_attr
    def __plural_label__(self):
        return pluralize(self.__label__)

    @classmethod
    def all(cls):
        """Get all models."""
        return cls.query.all()

    @classmethod
    def all2dict(cls):
        return [model_dict_clear(each.__dict__) for each in cls.query.all()]

    @classmethod
    def paginate(cls, *args, **kwargs):
        return cls.query.paginate(*args, **kwargs)

    @classmethod
    def get(cls, id):
        """Get one model by ID.

        :param id: The model ID to get.
        """
        return cls.query.get(id)

    @classmethod
    def get_by(cls, **kwargs):
        """Get one model by keyword arguments.

        :param kwargs: The model attribute values to filter by.
        """
        return cls.query.filter_by(**kwargs).first()

    @classmethod
    def get_or_create(cls, commit=False, **kwargs):
        """Get or create model by keyword arguments.

        :param bool commit: Whether or not to immediately commit the DB session (if create).
        :param kwargs: The model attributes to get or create by.
        """
        instance = cls.get_by(**kwargs)
        if not instance:
            instance = cls.create(**kwargs, commit=commit)
            has_instance = 0
        has_instance = 1
        return has_instance, instance

    @classmethod
    def join(cls, *props, **kwargs):
        return cls.query.join(*props, **kwargs)

    @classmethod
    def filter(cls, *args, **kwargs):
        return cls.query.filter(*args, **kwargs)

    @classmethod
    def filter_by(cls, **kwargs):
        """Find models by keyword arguments.

        :param kwargs: The model attribute values to filter by.
        """
        return cls.query.filter_by(**kwargs)

    @classmethod
    def create(cls, commit=False, **kwargs):
        """Create a new model and add it to the database session.

        :param bool commit: Whether or not to immediately commit the DB session.
        :param kwargs: The model attribute values to create the model with.
        """
        can_create = {}
        for attr, value in kwargs.items():
            if attr in cls.__column_name_list__() and attr not in cls.__non_init_props__:
                can_create.update({attr: value})
        instance = cls(**can_create)
        return instance.save(commit)

    def update(self, commit=False, **kwargs):
        """Update fields on the model.

        :param bool commit: Whether or not to immediately commit the DB session.
        :param kwargs: The model attribute values to update the model with.
        """
        for attr, value in kwargs.items():
            if attr in self.__column_name_list__() and attr not in self.__non_update_prop_list__:
                setattr(self, attr, value)
        return self.save(commit)

    def save(self, commit=False):
        """Save the model.

        :param bool commit: Whether or not to immediately commit the DB session.
        """
        db.session.add(self)
        if commit:
            db.session.commit()
        return self

    def delete(self, commit=False):
        """Delete the model.

        :param bool commit: Whether or not to immediately commit the DB session.
        """
        db.session.delete(self)
        return commit and db.session.commit()

    @staticmethod
    def delete_all(model_list):
        try:
            for model in model_list:
                db.session.delete(model)
            db.session.commit()
        except Exception as e:
            return 1
        else:
            return 0

    @staticmethod
    def save_all(model_list):
        try:
            db.session.add_all(model_list)
            db.session.commit()
        except Exception as e:
            return 1
        else:
            return 0

    @staticmethod
    def save_and_remove_all(add_model_list, delete_model_list):
        try:
            if add_model_list:
                db.session.add_all(add_model_list)
            if delete_model_list:
                for each in delete_model_list:
                    db.session.delete(each)
            db.session.commit()
        except Exception as e:
            return 1
        else:
            return 0

    @classmethod
    def fuzzy_search(cls, field, search):
        return cls.filter(field.like('%' + search + '%')).all()

    def __repr__(self):
        properties = [f'{prop}={getattr(self, prop)!r}'
                      for prop in self.__repr_props__ if hasattr(self, prop)]
        return f"<{self.__class__.__name__} {' '.join(properties)}>"
